﻿' Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

Imports System.Collections.Immutable
Imports System.Runtime.InteropServices
Imports System.Text
Imports Microsoft.CodeAnalysis.Text

Namespace Microsoft.CodeAnalysis.VisualBasic
    Friend NotInheritable Class CommandLineDiagnosticFormatter
        Inherits VisualBasicDiagnosticFormatter

        Private ReadOnly _baseDirectory As String
        Private ReadOnly _getAdditionalTextFiles As Func(Of ImmutableArray(Of AdditionalTextFile))

        Friend Sub New(baseDirectory As String, getAdditionalTextFiles As Func(Of ImmutableArray(Of AdditionalTextFile)))
            _baseDirectory = baseDirectory
            _getAdditionalTextFiles = getAdditionalTextFiles
        End Sub

        ' Returns a diagnostic message in string.
        ' VB has a special implementation that prints out a squiggle under the error span as well as a diagnostic message.
        ' e.g.,
        '   c:\Roslyn\Temp\a.vb(5) : warning BC42024: Unused local variable: 'x'.
        '
        '       Dim x As Integer
        '           ~
        '
        Public Overrides Function Format(diagnostic As Diagnostic, Optional formatter As IFormatProvider = Nothing) As String
            ' Builds a diagnostic message
            ' Dev12 vbc prints raw paths -- relative and not normalized, so we don't need to customize the base implementation.

            Dim text As SourceText = Nothing
            Dim diagnosticSpanOpt = GetDiagnosticSpanAndFileText(diagnostic, text)

            If Not diagnosticSpanOpt.HasValue OrElse
                text Is Nothing OrElse
                text.Length < diagnosticSpanOpt.Value.End Then
                ' Diagnostic either has Location.None OR an invalid location OR invalid file contents.
                ' For all these cases, format the diagnostic as a no-location project level diagnostic.

                ' Strip location, if required.
                If diagnostic.Location <> Location.None Then
                    diagnostic = diagnostic.WithLocation(Location.None)
                End If

                ' Add "vbc : " command line prefix to the start of the command line diagnostics which do not have a location to match the 
                ' behavior of native compiler.    This allows MSBuild output to be consistent whether Roslyn is installed or not.      
                Return VisualBasicCompiler.VbcCommandLinePrefix &
                    MyBase.Format(diagnostic, formatter)
            End If

            Dim baseMessage = MyBase.Format(diagnostic, formatter)

            Dim sb As New StringBuilder()
            sb.AppendLine(baseMessage)

            ' the squiggles are displayed for the original (unmapped) location
            Dim sourceSpan = diagnosticSpanOpt.Value
            Dim sourceSpanStart = sourceSpan.Start
            Dim sourceSpanEnd = sourceSpan.End
            Dim linenumber = text.Lines.IndexOf(sourceSpanStart)
            Dim line = text.Lines(linenumber)

            If sourceSpan.IsEmpty AndAlso line.Start = sourceSpanEnd AndAlso linenumber > 0 Then
                ' Sometimes there is something missing at the end of the line, then the error is reported with an empty span
                ' beyond the end of the line, which makes it appear that the span starts at the beginning of the next line.
                ' Let's go back to the previous line in this case.
                linenumber -= 1
                line = text.Lines(linenumber)
            End If

            While (line.Start < sourceSpanEnd)
                ' Builds the original text line
                sb.AppendLine()
                sb.AppendLine(line.ToString().Replace(vbTab, "    ")) ' normalize tabs with 4 spaces

                ' Builds leading spaces up to the error span
                For position = Math.Min(sourceSpanStart, line.Start) To Math.Min(line.End, sourceSpanStart) - 1
                    If (text(position) = vbTab) Then
                        ' normalize tabs with 4 spaces
                        sb.Append(" "c, 4)
                    Else
                        sb.Append(" ")
                    End If
                Next

                ' Builds squiggles
                If sourceSpan.IsEmpty Then
                    sb.Append("~")
                Else
                    For position = Math.Max(sourceSpanStart, line.Start) To Math.Min(If(sourceSpanEnd = sourceSpanStart, sourceSpanEnd, sourceSpanEnd - 1), line.End - 1)
                        If (text(position) = vbTab) Then
                            ' normalize tabs with 4 spaces
                            sb.Append("~"c, 4)
                        Else
                            sb.Append("~")
                        End If
                    Next
                End If

                ' Builds trailing spaces up to the end of this line
                For position = Math.Min(sourceSpanEnd, line.End) To line.End - 1
                    If (text(position) = vbTab) Then
                        ' normalize tabs with 4 spaces
                        sb.Append(" "c, 4)
                    Else
                        sb.Append(" ")
                    End If
                Next

                ' The error span can continue over multiple lines
                linenumber = linenumber + 1
                If linenumber >= text.Lines.Count Then
                    ' Exit the loop when we reach the end line (0-based)
                    Exit While
                End If

                line = text.Lines(linenumber)
            End While

            Return sb.ToString()
        End Function

        Friend Overrides Function FormatSourcePath(path As String, basePath As String, formatter As IFormatProvider) As String
            Return If(FileUtilities.NormalizeRelativePath(path, basePath, _baseDirectory), path)
        End Function

        Private Function GetDiagnosticSpanAndFileText(diagnostic As Diagnostic, <Out> ByRef text As SourceText) As TextSpan?
            If diagnostic.Location.IsInSource Then
                text = diagnostic.Location.SourceTree.GetText()
                Return diagnostic.Location.SourceSpan
            End If

            If diagnostic.Location.Kind = LocationKind.ExternalFile Then
                Dim path = diagnostic.Location.GetLineSpan().Path
                If path IsNot Nothing Then
                    For Each additionalTextFile In _getAdditionalTextFiles()
                        If path.Equals(additionalTextFile.Path) Then
                            Try
                                text = additionalTextFile.GetText()
                            Catch
                                text = Nothing
                            End Try

                            Return diagnostic.Location.SourceSpan
                        End If
                    Next
                End If
            End If

            text = Nothing
            Return Nothing
        End Function
    End Class
End Namespace
